Telegram 传声筒:利用 Cloudflare 免费搭建带人机验证的私聊机器人

如果你想在 Telegram 上开启私聊,但又担心被无穷无尽的广告机器人骚扰,那么搭建一个“自带门卫”的机器人是最佳选择。本文将手把手教你如何利用 Cloudflare Workers 零成本搭建一个支持人机验证的双向传声筒。

🌟 为什么选择这个方案?

  1. 完全免费:利用 Cloudflare 强大的免费额度,每天支撑几万次对话毫无压力。
  2. 拒绝骚扰:集成 Cloudflare Turnstile 人机验证,广告机器人无法通过验证,你的私聊列表将变得清净。
  3. 隐私保护:访客通过机器人联系你,无需暴露你的真实 Telegram 账号。
  4. 管理方便:支持管理员直接在 TG 里回复、拉黑,操作极其简单。

🛠️ 第一步:准备必要数据

在开始搭建前,请先收集以下信息:

  • 机器人 Token:找 @BotFather 创建机器人获取。
  • 管理员 ID:找 @userinfobot 获取你的数字 ID。
  • Cloudflare 账号:用于运行脚本和数据库。

🛡️ 第二步:申请人机验证 (Turnstile) 密钥

  1. 登录 Cloudflare 控制台,点击左侧 Turnstile
  2. 点击 添加站点,名称随便填,域名填你 Worker 的预览域名(如 xxx.workers.dev)。
  3. 小组件模式选择 托管 (Managed)
  4. 创建后保存好 站点密钥 (Site Key)秘密密钥 (Secret Key)

📦 第三步:配置 Cloudflare 后台

1. 创建 KV 数据库

在 Cloudflare 侧边栏找到 存储和数据库 -> KV,创建一个名为 nfd 的命名空间。

2. 创建并配置 Worker

创建一个新的 Worker,并进入其管理页面,点击 设置 (Settings) -> 变量 (Variables)

  • 添加 KV 绑定:变量名称填 nfd,选择你刚才创建的数据库。
  • 添加环境变量
    • ENV_BOT_TOKEN:你的机器人 Token。
    • ENV_BOT_SECRET:随便填一串字符,用于 Webhook 安全。
    • ENV_ADMIN_UID:你的 Telegram 数字 ID。
    • ENV_CF_SITE_KEY:Turnstile 站点密钥。
    • ENV_CF_SECRET_KEY:Turnstile 秘密密钥。

💻 第四步:部署核心脚本

在 Worker 的编辑器中,清空原有内容,粘贴以下代码并点击 部署

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
const TOKEN = ENV_BOT_TOKEN;      
const WEBHOOK = '/endpoint';
const SECRET = ENV_BOT_SECRET;
const ADMIN_UID = ENV_ADMIN_UID;
const VERIFICATION_TTL = 60 * 60 * 24 * 30;
const CF_TURNSTILE_SITE_KEY = ENV_CF_SITE_KEY;
const CF_TURNSTILE_SECRET_KEY = ENV_CF_SECRET_KEY;

function apiUrl(methodName, params = null) {
let query = '';
if (params) {
query = '?' + new URLSearchParams(params).toString();
}
return `https://api.telegram.org/bot${TOKEN}/${methodName}${query}`;
}

function requestTelegram(methodName, body, params = null) {
return fetch(apiUrl(methodName, params), {
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify(body)
}).then(r => r.json());
}

function sendMessage(msg = {}) {
return requestTelegram('sendMessage', msg);
}

function copyMessage(msg = {}) {
return requestTelegram('copyMessage', msg);
}

function forwardMessage(msg) {
return requestTelegram('forwardMessage', msg);
}

addEventListener('fetch', event => {
const url = new URL(event.request.url);
if (url.pathname === WEBHOOK) {
event.respondWith(handleWebhook(event, url));
} else if (url.pathname === '/registerWebhook') {
event.respondWith(registerWebhook(event, url, WEBHOOK, SECRET));
} else if (url.pathname === '/verify') {
event.respondWith(handleVerifyPage(event.request));
} else if (url.pathname === '/verify-callback') {
event.respondWith(handleVerifyCallback(event.request));
} else {
event.respondWith(new Response('No handler for this request'));
}
});

async function handleWebhook(event, url) {
if (event.request.headers.get('X-Telegram-Bot-Api-Secret-Token') !== SECRET) {
return new Response('Unauthorized', { status: 403 });
}
const update = await event.request.json();
event.waitUntil(onUpdate(update, url.origin));
return new Response('Ok');
}

async function onUpdate(update, origin) {
if ('message' in update) {
await onMessage(update.message, origin);
}
}

async function onMessage(message, origin) {
const chatId = message.chat.id.toString();
if (chatId === ADMIN_UID) {
return handleAdminMessage(message);
} else {
const isBlocked = await nfd.get('blocked-' + chatId);
if (isBlocked) {
return sendMessage({ chat_id: chatId, text: '🚫 您已被管理员拉黑。' });
}
const isVerified = await nfd.get('verified-' + chatId);
if (isVerified) {
return handleGuestMessage(message);
} else {
return handleVerification(message, chatId, origin);
}
}
}

async function handleAdminMessage(message) {
const text = (message.text || '').trim();
const reply = message.reply_to_message;
if (text === '/block') {
if (reply) {
const guestId = await nfd.get('msg-map-' + reply.message_id);
if (guestId) {
await nfd.put('blocked-' + guestId, 'true');
return sendMessage({ chat_id: ADMIN_UID, text: `🚫 用户 ${guestId} 已被拉黑。` });
}
}
return sendMessage({ chat_id: ADMIN_UID, text: '⚠️ 请回复一条转发的消息进行拉黑。' });
}
if (reply) {
const guestId = await nfd.get('msg-map-' + reply.message_id);
if (guestId) {
return copyMessage({
chat_id: guestId,
from_chat_id: message.chat.id,
message_id: message.message_id,
});
}
}
return sendMessage({ chat_id: ADMIN_UID, text: '🤖 管理面板:回复消息即发送,发送 /block 拉黑。' });
}

async function handleVerification(message, chatId, origin) {
const verifyUrl = `${origin}/verify?uid=${chatId}`;
return sendMessage({
chat_id: chatId,
text: '🛡 为了防止垃圾消息,请点击按钮完成验证:',
reply_markup: { inline_keyboard: [[{ text: '🤖 开始人机验证', web_app: { url: verifyUrl } }]] }
});
}

function handleVerifyPage(request) {
const html = `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>验证</title><script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script><script src="https://telegram.org/js/telegram-web-app.js"></script><style>body { font-family: sans-serif; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #f0f2f5; }.card { background: white; padding: 2rem; border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); text-align: center; }</style></head><body><div class="card"><h2>人机验证</h2><div class="cf-turnstile" data-sitekey="${CF_TURNSTILE_SITE_KEY}" data-callback="onVerify"></div><p id="status"></p></div><script>const tg = window.Telegram.WebApp; tg.ready(); function onVerify(token) { const uid = new URLSearchParams(window.location.search).get('uid'); fetch('/verify-callback', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token, uid }) }).then(res => { if (res.ok) { document.getElementById('status').innerText = "✅ 验证成功!"; setTimeout(() => tg.close(), 1500); } }); }</script></body></html>`;
return new Response(html, { headers: { 'content-type': 'text/html;charset=UTF-8' } });
}

async function handleVerifyCallback(request) {
const { token, uid } = await request.json();
const formData = new FormData();
formData.append('secret', CF_TURNSTILE_SECRET_KEY);
formData.append('response', token);
const result = await fetch('[https://challenges.cloudflare.com/turnstile/v0/siteverify](https://challenges.cloudflare.com/turnstile/v0/siteverify)', { method: 'POST', body: formData }).then(r => r.json());
if (result.success) {
await nfd.put('verified-' + uid, 'true', { expirationTtl: VERIFICATION_TTL });
await sendMessage({ chat_id: uid, text: '✅ 验证通过!现在您可以发送消息了。' });
return new Response('ok');
}
return new Response('fail', { status: 400 });
}

async function handleGuestMessage(message) {
const forwardReq = await forwardMessage({ chat_id: ADMIN_UID, from_chat_id: message.chat.id, message_id: message.message_id });
if (forwardReq.ok) {
await nfd.put('msg-map-' + forwardReq.result.message_id, message.chat.id.toString(), { expirationTtl: 172800 });
}
}

async function registerWebhook(event, requestUrl, suffix, secret) {
const webhookUrl = `${requestUrl.protocol}//${requestUrl.hostname}${suffix}`;
const r = await (await fetch(apiUrl('setWebhook', { url: webhookUrl, secret_token: secret }))).json();
return new Response(JSON.stringify(r));
}


🚀 第五步:激活并测试

  1. 在浏览器访问 https://你的域名/registerWebhook,看到 {"ok":true...} 说明连接成功。
  2. 用另一个账号给机器人发消息,测试是否会弹出验证。
  3. 作为管理员,收到转发消息后点击“回复”,测试对方是否能收到回信。

💡 核心知识点

  • Webhook 机制:让 Telegram 服务器在收到消息时主动推送到你的 Cloudflare Worker。
  • KV 存储:作为机器人的记忆,存储验证状态和消息映射关系。
  • Turnstile:Cloudflare 提供的免费人机识别技术,比传统的图片验证码更智能、更简单。